注意:所有文章除特别说明外,转载请注明出处.
第18章 LocaleResolver
LocaleResolver的主要作用在于根据不同的用户区域展示不同的视图,通过设置系统的环境,根据运行环境使用不同的语言显示。用户的区域称为 Locale ,这一信息可以由前端直接获取。通过这一方式可以实现国际化的目的。如:针对美国用户提供一个视图,针对中国用户提供一个视图。而LocaleResolver的使用是实现对用户不同视图的切换。LocaleResolver的作用是使用request解析出Locale。在LocaleResolver的实现类中,AcceptHeaderLocaleResolver直接使用了Header里的acceptlanguage值,不能在程序里面修改。
1.Spring针对LocaleResoler提供几种实现方式
1.FixedLocaleResolver 在声明该resolver时,需要指定一个默认的Locale,在进行Locale获取时,始终返回该Locale,并且调用其setLocale()方法也无法改变其Locale。
2.SessionLocaleResolver 其会将Locale信息存储在session中,如果用户想要修改Locale信息,可以通过修改session中对应属性的值即可。
3.CookieLocaleResolver 其读取Locale的方式是在session中通过Cookie来获取其指定的Locale的,如果修改了Cookie的值,页面视图也会同步切换。
4.AcceptHeaderLocaleResolver 其会通过用户请求中名称为Accept-Language的header来获取Locale信息,如果想要修改展示的视图,只需要修改该header信息即可。
提示:在Spring4.x之后,LocaleResolver添加子接口 LocaleContextResolver 其中增加了获取和设置LocaleContext 的能力,并添加了抽象类 AbstractLocaleContextResolver,抽象类添加了对TimeZone(时区)的支持。
注意:Spring虽然提供几个不同的获取Locale的方式,但是这些方式除 FixedLocaleResolver 以外,其它也都支持在浏览器地址栏添加Locale参数来切换Locale。而对于Locale的切换,Spring通过拦截器来实现,其提供一个LocaleChangeInterceptor,在该拦截器中的preHandler()方法中,Spring会读取浏览器参数中的locale参数,然后调用LocaleResolver.setLocale()方法实现对Locale的切换。
2.LocaleResolver接口声明
public interface LocaleResolver {
// 根据request对象根据指定的方式获取一个Locale,如果没有获取到,则使用用户指定的默认的Locale
Locale resolveLocale(HttpServletRequest request);
// 用于实现Locale的切换。比如SessionLocaleResolver获取Locale的方式是从session中读取,但如果
// 用户想要切换其展示的样式(由英文切换为中文),那么这里的setLocale()方法就提供了这样一种可能
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response,
@Nullable Locale locale);
}
2.1 重写 resolveLocale(HttpServletRequest request)方法
这里使用默认的解析器 AcceptHeaderLocaleResolver 继承 LocaleResolver 重写resolverLocale(HttpServletRequest request)方法,通过检查客户端发送请求中的Accept-Language头来确定客户端Locale(地区信息)。
/**
* 从当前的request中解析Locale
*/
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取默认设置,可在配置AcceptHeaderLocaleResolver Bean中设置defaultLocale属性
Locale defaultLocale = getDefaultLocale();
// 设置了默认值并且请求中没有Accept-Language头信息时,使用默认设置
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
// 从当前请求中获取Locale
Locale requestLocale = request.getLocale();
// 从配置中获取支持的Locale集合,可在AcceptHeaderLocaleResolver Bean中设置supportedLocales属性
List<Locale> supportedLocales = getSupportedLocales();
// 未设置supportedLocales或者supportedLocales中包括请求Locale,则使用请求Locale
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
// 找到设置的Locale集合中是否有请求的Locale
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
/**
* 不支持程序设置Locale
*/
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
throw new UnsupportedOperationException(
"Cannot change HTTP accept header - use a different locale resolution strategy");
}
2.2 在SpringMVC配置文件中配置资源加载以及AcceptHeaderLocaleResolver bean
<!-- 国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<!-- 如果资源文件放在classpath下,basename的value必须有classpath:前缀,否则报错:No message found under code... -->
<property name="basename" value="classpath:i18n/messages" />
<!-- 如果在国际化资源文件中找不到对应代码的信息,就用这个代码作为名称返回 -->
<property name="useCodeAsDefaultMessage" value="true" />
<!--<property name="defaultEncoding" value="ISO-8859-1"/>-->
</bean>
<!-- LocaleResolver解析器 -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver">
<property name="defaultLocale" value="zh_CN"/>
</bean>
2.3 属性文件
2.3.1 messages_en.properties
message.locale = en
2.3.2 messages_zh_CN.properties
message.locale=zh_CN
2.4 Controller
@GetMapping(value = "/acceptHeaderLocaleResolver" , produces = "text/html;charset=UTF-8")
@ResponseBody
public String test(HttpServletRequest request) {
String clientLocale = "";
Enumeration<Locale> enus = request.getLocales();
while (enus.hasMoreElements()){
Locale locale = enus.nextElement();
clientLocale += locale + ",";
}
RequestContext requestContext = new RequestContext(request);
String value = requestContext.getMessage("message.locale");
return "客户端支持的Locale有:"+clientLocale+" </br>当前使用的Locale是:" + requestContext.getLocale() + " </br>使用的资源Locale文件是:" + value ;
}
3.CookieLocaleResolver解析器
CookieLocaleResolver解析器通过不同的Locale展示不同的视图。CookieLocaleResolver类的入口是:resolveLocaleContext(final HttpServletRequest request); SpringMVC接收到客户端请求之后会调用该方法。
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
// 解析Cookie信息
parseLocaleCookieIfNecessary(request);
// 返回Locale和TimeZone
return new TimeZoneAwareLocaleContext() {
@Override
@Nullable
public Locale getLocale() {
return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
}
@Override
@Nullable
public TimeZone getTimeZone() {
return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
}
};
}
private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
// 第一次请求为null
if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) == null) {
Locale locale = null; // 地区
TimeZone timeZone = null; // 时区
// 获取cookie的名称,取自Spring MVC配置,默认为:CookieLocaleResolver.DEFAULT_COOKIE_NAME
String cookieName = getCookieName();
if (cookieName != null) {
// 根据名称获取当前请求中的Cookie(第一次访问为null)
Cookie cookie = WebUtils.getCookie(request, cookieName);
if (cookie != null) {
// 以下主要是从客户端Cookie中解析出Locale
String value = cookie.getValue();
String localePart = value;
String timeZonePart = null;
int spaceIndex = localePart.indexOf(' ');
if (spaceIndex != -1) {
localePart = value.substring(0, spaceIndex);
timeZonePart = value.substring(spaceIndex + 1);
}
try {
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
if (timeZonePart != null) {
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
}
}
catch (IllegalArgumentException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
// Error dispatch: ignore locale/timezone parse exceptions
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale cookie '" + cookieName +
"' with value [" + value + "] due to error dispatch: " + ex.getMessage());
}
}
else {
throw new IllegalStateException("Invalid locale cookie '" + cookieName +
"' with value [" + value + "]: " + ex.getMessage());
}
}
if (logger.isDebugEnabled()) {
logger.debug("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
}
}
}
// 把Locale设置到请求的Attribute区,客户端请求没有携带Cookie,取Spring MVC中配置的defaultLocale
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
(locale != null ? locale : determineDefaultLocale(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}
}
// 设置Locale
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}
// 主要是把Locale信息写回客户端
@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
@Nullable LocaleContext localeContext) {
Assert.notNull(response, "HttpServletResponse is required for CookieLocaleResolver");
Locale locale = null;
TimeZone timeZone = null;
if (localeContext != null) {
locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext) {
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
}
addCookie(response,
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
}
else {
removeCookie(response);
}
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
(locale != null ? locale : determineDefaultLocale(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}
3.1 配置xml文件
<context:component-scan base-package="mvc"/>
<mvc:annotation-driven/>
<mvc:interceptors>
<!--添加拦截器,用于对Locale的切换-->
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<bean class="mvc.interceptor.MyHandlerInterceptor"/>
</mvc:interceptors>
<!--指定ViewResolver是ResourceBundleViewResolver是因为其支持通过不同的Locale进行不同的视图切换-->
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver"/>
<!--指定LocaleResolver为CookieLocaleResolver-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<!--指定defaultlocal是zh_CN-->
<property name="defaultLocale" value="zh_CN"/>
</bean>
3.2 声明接口
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detail(@RequestParam("id") long id, @ModelAttribute("message") String message, Locale locale) {
System.out.println(message);
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
view.addObject("locale", locale);
return view;
}
}
提示:这里返回的视图是:user 并将Locale信息返回给前端。这里获取Locale数据的方式就只需要简单的声明一个类型为Locale的参数。
3.3 视图的展示
由于需要根据不同的Locale展示不同的视图,我们在上面的接口中没有发现这样的路由。在实际中,这个路由是根据ResourceBundleViewResolver类实现的,在使用该ViewResolver时,其会到class路径下查找名称为views的Resource Bundle,同时通过用户指定的Locale,唯一定位到某个 Resource Bundle。 然后在该Resource Bundle 中查找指定的视图信息。
总结
这里我们的LocaleResolver的bean名称必须是 localeResolver,并且需要指定的 ViewResolver 辅以支持,否则切换的视图可能无法工作。